﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Glue.Internals.Nulls;

namespace Glue.Internals
{
    internal class RelationContainer<LeftType,RightType>
    {
        private readonly Dictionary<PropertyPair, IRelation> relations = new Dictionary<PropertyPair, IRelation>();
        private readonly MemberContainerWrapperForRelationContainer members = MemberContainerWrapperForRelationContainer.CreateContainer();
        private readonly ObjectMemberFactory objectMemberFactory;

        public RelationContainer(ObjectMemberFactory objectMemberFactory)
        {
            this.objectMemberFactory = objectMemberFactory;
        }

        public int Count
        {
            get { return relations.Count; }
        }

        public void Add(IRelation relation)
        {
            var key = GetDictionaryKey(relation);
            if (relations.ContainsKey(key))
            {
                members.Remove(relations[key]);
                relations.Remove(key);
            }
            else 
            {
                if (relation.HasTowardsLeft())
                    AssertValidTowardsLeft(relation);

                if (relation.HasTowardsRight())
                    AssertValidTowardsRight(relation);
            }
            members.Add(relation);
            relations.Add(key, relation);
        }

        private void AssertValidTowardsRight(IRelation relation)
        {
            var member = relation.RightMember;

            var basePropertyHolder = member.GetParentProperty();
            if (basePropertyHolder.HasValue() && members.IsRelatedTowardsOnRightSideWithNestedMapping(basePropertyHolder.GetValue().BaseMemberName))
                throw new GlueException(String.Format("The member '{0}' is already related through a nested mapping, so using the simplified nested mapping '{1}' can't be done here. You can use just one of them. Either add this mapping to the nested mapping, or add all relations as simplified nested mappings.", typeof(RightType).Name + "." + basePropertyHolder.GetValue().BaseMemberName, member.Name));

            if (member is INestedMember && members.IsRelatedTowardsOnRightSideWithSimplifiedNestedMapping(member))
                throw new GlueException(String.Format("The member '{0}' is already related through on or more simplified nested mappings, so using a mapping can't be done here. You can use just one of them. Either add all the mappings to the nested mapping, or add all relations as simplified nested mappings", member.Name));

            if (members.IsRelatedTowardsOnRightSide(member))
                throw new GlueException("The member '" + typeof(RightType).Name + "." + member.Name + "' is already related towards. It can't be related towards twice.");
        }

        private void AssertValidTowardsLeft(IRelation relation)
        {
            var member = relation.LeftMember;
            var basePropertyHolder = member.GetParentProperty();
            if (basePropertyHolder.HasValue() && members.IsRelatedTowardsOnLeftSideWithNestedMapping(basePropertyHolder.GetValue().BaseMemberName))
                throw new GlueException(String.Format("The member '{0}' is already related through a nested mapping, so using the simplified nested mapping '{1}' can't be done here. You can use just one of them. Either add this mapping to the nested mapping, or add all relations as simplified nested mappings.", typeof(LeftType).Name + "." + basePropertyHolder.GetValue().BaseMemberName, member.Name));

            if (member is INestedMember && members.IsRelatedTowardsOnLeftSideWithSimplifiedNestedMapping(member))
                throw new GlueException(String.Format("The member '{0}' is already related through on or more simplified nested mappings, so using a mapping can't be done here. You can use just one of them. Either add all the mappings to the nested mapping, or add all relations as simplified nested mappings", member.Name));

            if (members.IsRelatedTowardsOnLeftSide(member))
                throw new GlueException("The member '" + typeof(LeftType).Name + "." + member.Name + "' is already related towards. It can't be related towards twice.");
        }

        public List<IRelation> GetRelationsTowardsLeft()
        {
            return GetRelations(relation=>relation.HasTowardsLeft());
        }

        public List<IRelation> GetRelationsTowardsRight()
        {
            return GetRelations(relation=>relation.HasTowardsRight());
        }

        public IEnumerable<IRelation> GetAllRelations()
        {
            return GetRelations(x => true);
        }

        public IRelation Get(Expression<Func<LeftType, object>> left, Expression<Func<RightType, object>> right)
        {
            var leftProperty = objectMemberFactory.CreateObjectMember(left);
            var rightProperty = objectMemberFactory.CreateObjectMember(right);
            var relationMaybe = GetRelation(
                    x => x.LeftMember.Name == leftProperty.Name && x.RightMember.Name == rightProperty.Name);

            if (!relationMaybe.HasValue())
                throw new GlueException("No relation between '" + typeof(LeftType).Name + "." + leftProperty.Name + "' and '" + typeof(RightType).Name + "." + rightProperty.Name + "' exist");
            return relationMaybe.GetValue();
        }

        public void Remove(Expression<Func<LeftType, object>> left, Expression<Func<RightType, object>> right)
        {
            var relation = Get(left, right);
            relations.Remove(GetDictionaryKey(relation));
            members.Remove(relation);
        }

        public bool IsRelatedTowardsOnLeftSide(IObjectMember member)
        {
            return members.IsRelatedTowardsOnLeftSide(member);
        }

        public bool IsRelatedTowardsOnRightSide(IObjectMember member)
        {
            return members.IsRelatedTowardsOnRightSide(member);
        }

        private PropertyPair GetDictionaryKey(IRelation relation)
        {
            return new PropertyPair(relation.LeftMember, relation.RightMember);
        }

        private Maybe<IRelation> GetRelation(Func<IRelation, bool> func)
        {
            var foundRelations = GetRelations(func);
            return foundRelations.Count == 0 ? Maybe<IRelation>.Nothing() : new Maybe<IRelation>(foundRelations[0]);
        }

        private List<IRelation> GetRelations(Func<IRelation, bool> func)
        {
            return new List<IRelation>(FindRelations(func));
        }

        private IEnumerable<IRelation> FindRelations(Func<IRelation, bool> func)
        {
            var enumerator = relations.Values.GetEnumerator();
            while (enumerator.MoveNext())
            {
                if (func(enumerator.Current))
                    yield return enumerator.Current;
            }
        }

        private class PropertyPair
        {
            private readonly IObjectMember leftMember;
            private readonly IObjectMember rightMember;

            public PropertyPair(IObjectMember leftMember, IObjectMember rightMember)
            {
                this.leftMember = leftMember;
                this.rightMember = rightMember;
            }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof (PropertyPair)) return false;
                return Equals((PropertyPair) obj);
            }

            public bool Equals(PropertyPair obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                return Equals(obj.leftMember.Name, leftMember.Name) && Equals(obj.rightMember.Name, rightMember.Name);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return (leftMember.Name.GetHashCode()*397) ^ rightMember.Name.GetHashCode();
                }
            }

            public static bool operator ==(PropertyPair left, PropertyPair right)
            {
                return Equals(left, right);
            }

            public static bool operator !=(PropertyPair left, PropertyPair right)
            {
                return !Equals(left, right);
            }
        }
    }
}
